package cz.mff.cuni.dpp.liboptions;

import java.util.Arrays;
import java.util.Map.Entry;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * neni viditelny zvenci, parsuje argumenty.
 * 
 * 
 * 
 */
class OptionParser {

	private static final Logger logger = LoggerFactory.getLogger(OptionParser.class);

	private static final String PUBLISH_BAD_VALUE_NULL = "null";
	private static final String PUBLISH_BAD_VALUE_EMPTY = "\"\"";

	private enum State {
		OPTION, VALUE,
		/**
		 * Vlastní parametry volání, tedy ne optiony
		 */
		ARGS
	}

	private Result result;

	private Option<?> lastOption;

	private State state = State.OPTION;

	private Options options;

	private OptionParser(Options options) {
		this.options = options;
		result = new Result(options.getOptions());
	}

	static Result parse(Options options, String[] tokens) {
		return new OptionParser(options).parseInternal(tokens);
	}

	private Result parseInternal(String[] tokens) {
		logger.debug("Parsing started for Command of length: " + Arrays.toString(tokens));

		for (String token : tokens) {
			handleSingleArgument(token);
		}

		logger.debug("Going to validate results");
		result.validate();

		logErrors();

		return result;
	}

	private void logErrors() {
		if (result.getErrors().getErrorsCount() == 0) {
			logger.info("No errors found during parsing");
		} else {
			logger.warn("We have found " + result.getErrors().getErrorsCount() + " during parsing");
			if (result.getErrors().getBadValue().size() > 0) {
				logger.warn("Bad values (option, value):");
				for (Entry<Option<?>, String> entry : result.getErrors().getBadValue().entrySet()) {
					logger.warn("\t" + entry.getKey().getPrimaryName() + ": " + entry.getValue());

				}
			}

			if (result.getErrors().getOutOfRange().size() > 0) {
				logger.warn("Out of range values (option, value):");
				for (Entry<Option<?>, String> entry : result.getErrors().getOutOfRange().entrySet()) {
					logger.warn("\t" + entry.getKey().getPrimaryName() + ": " + entry.getValue());

				}
			}

			if (result.getErrors().getMissingObligatory().size() > 0) {
				logger.warn("Missing options (option):");
				for (Option<?> option : result.getErrors().getMissingObligatory()) {
					logger.warn("\t" + option.getPrimaryName());
				}
			}

			if (result.getErrors().getMissingValue().size() > 0) {
				logger.warn("Missing values for present options (option):");
				for (Option<?> option : result.getErrors().getMissingValue()) {
					logger.warn("\t" + option.getPrimaryName());
				}
			}

			if (result.getErrors().getUnrecognizedOption().size() > 0) {
				logger.warn("Unrecognized options (option):");
				for (String string : result.getErrors().getUnrecognizedOption()) {
					logger.warn("\t" + string);
				}
			}
		}

	}

	private void handleSingleArgument(String token) {
		logger.debug("parsing token: " + token + " in state: " + state);
		if (state == State.ARGS) {
			if (Option.ARGUMENTS_DELIMITER.equals(token)) {
				logger.warn("Args delimiter while in args: " + token);
				result.getErrors().setBadAgrumentsDelimiterUsed(true);
			} else {
				parseArgument(token);
			}
		} else if (state == State.OPTION) {
			if (Option.ARGUMENTS_DELIMITER.equals(token)) {
				logger.debug("Switching to argument due to encounter of argument delimiter: "
						+ token);
				state = State.ARGS;
			} else if (token.startsWith(Option.LONG_OPTION_PREFIX)) {
				parseLongOption(token);
			} else if (token.startsWith(Option.SHORT_OPTION_PREFIX)) {
				parseShortOption(token);
			} else {
				state = State.ARGS;
				parseArgument(token);
			}
		} else if (state == State.VALUE) {
			// to ze je preskocena value o Optiony, ktera to potrebuje je
			// zkontrolovano v Errors.validate, proto to neni zde.
			if (Option.ARGUMENTS_DELIMITER.equals(token)) {
				state = State.ARGS;
			} else if (token.startsWith(Option.LONG_OPTION_PREFIX)) {
				state = State.OPTION;
				lastOption = null;
				parseLongOption(token);
			} else if (token.startsWith(Option.SHORT_OPTION_PREFIX)) {
				state = State.OPTION;
				lastOption = null;
				parseShortOption(token);
			} else {
				parseValue(token);
			}
		}

	}

	private void parseArgument(String token) {
		logger.debug("Parsing as agrument: " + token);
		result.addArgument(token);
	}

	private void parseValue(String token) {
		logger.debug("Parsing as value: " + token + " for option " + lastOption.getPrimaryName());

		// Z logiky se nemuze stat
		assert lastOption != null;
		if (token != null && !token.isEmpty()) {
			try {
				Object value = lastOption.parseValue(token);
				logger.debug("Value parsed as: " + value);
				result.putValue(lastOption, value);
			} catch (BadValueException e) {
				logger.warn("Problem with parsing of value: " + e.getStringValue()
						+ " isOutOfRange: " + e.isOutOfRange());
				if (e.isOutOfRange()) {
					result.getErrors().addOutOfRange(e.getOption(), e.getStringValue());
				} else {
					result.getErrors().addBadValue(e.getOption(), e.getStringValue());
				}
			}
		} else if (lastOption.supportsValue()) {
			logger.debug("Handling empty value: " + token);
			String publishValue = token == null ? PUBLISH_BAD_VALUE_NULL : PUBLISH_BAD_VALUE_EMPTY;
			logger.debug("Empty value will be saved as: " + publishValue);
			result.getErrors().addBadValue(lastOption, publishValue);
		}

		state = State.OPTION;
		lastOption = null;
	}

	private void parseLongOption(String token) {
		logger.debug("Parsing as longOption: " + token);
		String actualName = token.substring(Option.LONG_OPTION_PREFIX.length());
		String actualValue = null;
		if (actualName.contains(Option.LONG_OPTION_VALUE_DELIMITER)) {
			actualName = actualName.substring(0,
					actualName.indexOf(Option.LONG_OPTION_VALUE_DELIMITER));
			if (token.contains(Option.LONG_OPTION_VALUE_DELIMITER)) {
				// muze byt i prazdny!
				actualValue = token
						.substring(token.indexOf(Option.LONG_OPTION_VALUE_DELIMITER) + 1);
			}
		}

		logger.debug("using name: " + actualName);
		logger.debug("using value: " + actualValue);

		Option<?> option = findOptionByLongName(actualName);
		if (option != null) {
			result.addPresentOption(option);
			if (option.supportsValue()) {
				lastOption = option;
				state = State.VALUE;
				parseValue(actualValue);
			}
		} else {
			logger.warn("Option not found for name: " + actualName);
			result.getErrors().addUnrecognizedOption(token);
		}
	}

	private void parseShortOption(String token) {
		logger.debug("Parsing as shortOption: " + token);
		String actualName = token.substring(Option.SHORT_OPTION_PREFIX.length());
		Option<?> option = findOptionByShortName(actualName);

		logger.debug("using name: " + actualName);

		if (option != null) {
			result.addPresentOption(option);
			if (option.supportsValue()) {
				lastOption = option;
				state = State.VALUE;
			}
		} else {
			logger.warn("Option not found for name: " + actualName);
			result.getErrors().addUnrecognizedOption(token);
		}

	}

	private Option<?> findOptionByShortName(String actualName) {
		for (Option<?> option : options.getOptions()) {
			if (option.getShortNames().contains(actualName)) {
				return option;
			}
		}
		return null;
	}

	private Option<?> findOptionByLongName(String actualName) {
		for (Option<?> option : options.getOptions()) {
			if (option.getLongNames().contains(actualName)) {
				return option;
			}
		}
		return null;
	}

}
